Skip to content

Add Localization Features package for System.CommandLine #1013

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from

Conversation

fredrikhr
Copy link
Contributor

@fredrikhr fredrikhr commented Aug 6, 2020

TL;DR

  • Adds a new package System.CommandLine.Localization that contains functionality that extends System.CommandLine with localization features.
  • Adds a LocalizedHelpBuilder type that derives from the default HelpBuilder, but offers localized help output.
  • Adds a UseLocalization extension method on CommandLineBuilder.

Details

The new System.CommandLine.Localization package uses the Microsoft.Extensions.Localization package to provide localization features. By default the implementation uses the Resource Manager based IStringLocalizerFactory which provides localization from embedded resources that are compiled from .resx files at compile time. ref.: Globalization and Localization in ASP.NET Core

UseLocalization extension method

New API

namespace System.CommandLine.Builder
{
    partial class CommandLineBuilderExtensions
    {
        public partial static CommandLineBuilder UseLocalization(
            this CommandLineBuilder builder, Type? resourceSource = null);
    }
}

The UseLocalization extension method is the main entry point that makes the CommandLine application localization aware. In essence it does two things:

  1. It add a IStringLocalizerFactory to the BindingContext of the invocation.
  2. Calls UseHelpBuilder to setup the new LocalizedHelpBuilder that outputs help using the string localizer factory mentioned above.

The optional Type argument is used to specify from which assembly and resource file the strings are localized. If the Type is not specified, the Type that declares the entry point (i.e. the Main method) in the entry assembly is used.

If the Generic Host integration (System.CommandLine.Hosting) is being used and the binding context knows how to resolve an IHost instance, the extension method attempt to resolve the IStringLocalizerFactory from the service provider of the Generic Host. This will respect any configuration made in the Host setup with regards to logging and localization options. The reference to IHost is made via reflection, so that System.CommandLine.Localization does not need to depend on the Generic Host package.

LocalizedHelpBuilder class

New Type

using System.CommandLine.Help;

// New Namespace
namespace System.CommandLine.Localization
{
    public partial class LocalizedHelpBuilder : HelpBuilder
    {
        public LocalizedHelpBuilder(IStringLocalizerFactory localizerFactory,
            Type resourceSource, IConsole console, int? columnGutter = null,
            int? indentationSize = null, int? maxWidth = null)
        { }
    }
}

The LocalizedHelpBuilder class derives from the System.CommandLine help builder and uses its base implementation as much as possible. In order to localize the output of the static strings in the DefaultHelpText class (and some constant literals in HelpBuilder, System.Command.Localization also adds a LocalizedHelpBuilder.resx resource file that together with the XLF files provides localized strings for the Help output.

In order to localize the actual help on commands, options and arguments, the Localized Help Builder walks the command or option that it is writing output for and clones all symbols (effectively making localized clones). The Localized Help Builder then replaces the Description of each symbol with a localized description.

!string.IsNullOrEmpty(culture))
CultureInfo.CurrentCulture = CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo(culture);

var execCtx = ExecutionContext.Capture();
Copy link
Contributor

@jonsequitur jonsequitur Aug 6, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're using this as guidance, is there an approach that can work without using ExecutionContext? It's fairly heavy in terms of performance and a bit magical.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this is just a quick and dirty hack to achieve this in the playground, but I am using the same trick in #951, so we should discuss it there... When we merge #951 this won't be necessary anyways. In the meantime is it okay to just keep this here, since it's just the playground?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jonsequitur I improved it slightly in 8587a5c

@brettfo brettfo changed the base branch from master to main August 19, 2020 19:52
"profiles": {
"LocalizationPlayground": {
"commandName": "Project",
"commandLineArgs": "[env:DOTNET_SYSTEM_GLOBALIZATION_CULTURE=de-DE] -c 3 .NET"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This env should not be needed

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be a .net core leveled env to do the same thing

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wli3 We were hoping to align to whatever environment variables exist already in the SDK but the only one I could find was for specifying the invariant culture, not for specifying other cultures. https://docs.microsoft.com/en-us/dotnet/core/run-time-config/globalization

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my memory is wrong. https://github.com/dotnet/sdk/blob/b1223209644d900702287faea8e9b71f95ec49f8/src/Cli/dotnet/UILanguageOverride.cs#L38

we had a CLI specific flag. If we need a flag for this library that's fine.

@wli3
Copy link

wli3 commented Aug 20, 2020

I don't think xlf is generated by Arcade. We need to do that so it can fit into existing localization system.

But that's also a headache, we would require the contributor to run a build with arcade (which is disabled in "dev mode") and then checkin the new xlfs. Maybe the "right way" is we could have a bot to run and append a checkin for PRs touching resx files.

@jonsequitur
Copy link
Contributor

Related: #1125

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants